Skip to content

lltable: Fix use-after-free race in llentry_free()#2219

Open
TeddyEngel wants to merge 1 commit into
freebsd:mainfrom
TeddyEngel:fix/285813-arptimer-uaf-race
Open

lltable: Fix use-after-free race in llentry_free()#2219
TeddyEngel wants to merge 1 commit into
freebsd:mainfrom
TeddyEngel:fix/285813-arptimer-uaf-race

Conversation

@TeddyEngel
Copy link
Copy Markdown
Contributor

@TeddyEngel TeddyEngel commented May 21, 2026

Fix a use-after-free race condition in llentry_free() that causes kernel panics when ARP timer callbacks race with interface destruction.

Problem

When callout_stop() returns 0, the timer callback (e.g., arptimer()) is currently executing on another CPU. The original code proceeded to free the llentry anyway via LLE_FREE_LOCKED(), causing use-after-free when the timer callback subsequently accessed the freed memory.

The race window:

  1. Interface destruction path calls llentry_free()
  2. callout_stop(&lle->lle_timer) returns 0 (timer is executing)
  3. Original code proceeds to LLE_FREE_LOCKED(lle) - destroys lock, frees entry
  4. Meanwhile, arptimer() on another CPU tries to access the freed entry

Crash Signatures

This race manifests as various panics depending on timing:

Crash at lock acquisition (rw_lock = 0x6 = RW_DESTROYED):

#8  __rw_wlock_hard (c=0xfffff8001bc27d28) at kern_rwlock.c:1005
#9  arptimer (arg=0xfffff8001bc27c00) at if_ether.c:212

Crash at NULL dereference:

arptimer() at arptimer+0x6b
fault virtual address = 0x100000390

Zeroed callout wheel entry:

  • cc->cc_callwheel[] entry entirely zeroed out
  • Freed llentry while callout still referenced it

Solution

Check callout_stop() return value and handle each case:

  • > 0: Timer was pending and successfully cancelled. Drop timer's reference, proceed to free.
  • == 0: Timer is currently executing. Use refcnt to distinguish:
    • refcnt > 1: Another thread is racing with the timer. The entry is already unlinked (KASSERT), so the timer will skip its LLE_REMREF. Drop one ref and bail; timer's llentry_free() call will free the entry.
    • refcnt == 1: We are the timer callback and already dropped our ref. Proceed to free.
  • < 0: Timer was not scheduled. Proceed normally.

Problem Report

When callout_stop() returns 0, the timer callback is currently executing
on another CPU.  The original code proceeded to free the llentry anyway,
causing a use-after-free when the timer callback (e.g., arptimer) accessed
the freed memory.

Fix by checking the callout_stop() return value:
- If >0: timer was pending and successfully cancelled, drop timer's ref
- If 0 and refcnt>1: another thread racing with timer, drop ref and bail;
  timer's llentry_free() will free the entry
- If 0 and refcnt==1: we ARE the timer, proceed to free
- If <0: timer was not scheduled, proceed normally

PR:     285813
Signed-off-by: Teddy Engel <engel.teddy@gmail.com>
@TeddyEngel TeddyEngel force-pushed the fix/285813-arptimer-uaf-race branch from 39b5f4f to 6013202 Compare May 23, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant